﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using Nvigorate.Data;
using Nvigorate.Data.Query;
using Nvigorate.Extensions;
using Nvigorate.Relational.Mapping.Index;

namespace Nvigorate.Relational.Mapping
{
    public class Translator : IParameterize
    {
        #region Fields

        protected Dictionary<string, object> _valueLookup = new Dictionary<string, object>();
        protected IDatabaseProvider _provider;
        protected IRepositoryMapIndex _repositoryMapIndex;

        private static readonly Dictionary<Type, Func<IQueryObject, Translator, IQueryObject>> _visitors
            = new Dictionary<Type, Func<IQueryObject, Translator, IQueryObject>>()
                  {
                      { typeof (AssignmentColumn), ProcessAssignmentColumn },
                      { typeof (Between), ProcessBetween },
                      { typeof (Column), ProcessColumn },
                      { typeof (CriteriaClause), ProcessCriteriaClause },
                      { typeof (Criterion<>), ProcessCriterion },
                      { typeof (Delete), ProcessDelete },
                      { typeof (Equal), ProcessEqual },
                      { typeof (Function), ProcessFunction },
                      { typeof (Greater), ProcessGreater },
                      { typeof (GreaterOrEqual), ProcessGreaterOrEqual },
                      { typeof (IfBlock), ProcessIfBlock },
                      { typeof (In), ProcessIn },
                      { typeof (Insert), ProcessInsert },
                      { typeof (InsertColumn), ProcessInsertColumn },
                      { typeof (Less), ProcessLess },
                      { typeof (LessOrEqual), ProcessLessOrEqual },
                      { typeof (Like), ProcessLike },
                      { typeof (QueryList), ProcessQueryList },
                      { typeof (Literal), ProcessLiteral },
                      { typeof (NotEqual), ProcessNotEqual },
                      { typeof (NullQueryObject), ProcessNullQueryObject },
                      { typeof (Order), ProcessOrder },
                      { typeof (OrderColumn), ProcessOrderColumn },
                      { typeof (Page), ProcessPage },
                      { typeof (Property), ProcessProperty },
                      { typeof (RaiseError), ProcessRaiseError },
                      { typeof (Reference), ProcessReference },
                      { typeof (SchemaColumn), ProcessSchemaColumn },
                      { typeof (SearchCriteria), ProcessSearchCriteria },
                      { typeof (Select), ProcessSelect },
                      { typeof (Table), ProcessTable },
                      { typeof (TableVariable), ProcessTableVariable },
                      { typeof (Top), ProcessTop },
                      { typeof (Update), ProcessUpdate },
                      { typeof (Variable), ProcessVariable },
                      { typeof (Where), ProcessWhere },
                  };

        #endregion

        public string GetParameter(object value)
        {
            if (value == null ||
                value == System.DBNull.Value)
            {
                return "NULL";
            }
            if (_valueLookup.ContainsValue(value))
            {
                return _valueLookup.GetKeyForValue(value);
            }
            else
            {
                var param = string.Format("{0}{1}{2}", _provider.ParameterPrefix, "param", _valueLookup.Count + 1);
                _valueLookup.Add(param, value);
                return param;
            }
        }

        public List<IDbDataParameter> Parameterize(IQueryObject query, IDatabaseProvider provider, IRepositoryMapIndex repositoryMapIndex)
        {
            _provider = provider;
            _repositoryMapIndex = repositoryMapIndex;
            Process(query);
            return _valueLookup
                .Select(kvp =>
                    _provider.GetParameter(kvp.Key, kvp.Value, kvp.Value.GetType(), ParameterDirection.Input))
                .ToList();
        }

        public IQueryObject Process(IQueryObject queryObject)
        {
            try
            {
                var type = queryObject.GetType();
                return
                    type.IsGenericType ?
                    _visitors[type.GetGenericTypeDefinition()](queryObject, this) :
                    _visitors[type](queryObject, this);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                return null;
            }
        }

        #region Processors

        public static IQueryObject ProcessAssignmentColumn(IQueryObject queryObject, Translator parameterizer)
        {
            var column = queryObject as AssignmentColumn;
            column.Value = parameterizer.Process(column.Value);
            return column;
        }

        public static IQueryObject ProcessBetween(IQueryObject queryObject, Translator parameterizer)
        {
            var between = queryObject as Between;
            between.Field = parameterizer.Process(between.Field);
            between.Lower = parameterizer.Process(between.Lower);
            between.Upper = parameterizer.Process(between.Upper);
            return between;
        }

        public static IQueryObject ProcessColumn(IQueryObject queryObject, Translator parameterizer)
        {
            return queryObject;
        }

        public static IQueryObject ProcessCriteriaClause(IQueryObject queryObject, Translator parameterizer)
        {
            var clause = queryObject as CriteriaClause;
            clause.Criteria.ForEach(c => parameterizer.Process(c));
            return clause;
        }

        public static IQueryObject ProcessCriterion(IQueryObject queryObject, Translator parameterizer)
        {
            var criterion = queryObject as BaseCriterion;
            criterion.Field = parameterizer.Process(criterion.Field);
            return criterion;
        }

        public static IQueryObject ProcessDelete(IQueryObject queryObject, Translator parameterizer)
        {
            var delete = queryObject as Delete;
            parameterizer.Process(delete.ColumnList);
            parameterizer.Process(delete.Table);
            parameterizer.Process(delete.Qualifiers);
            return delete;
        }

        public static IQueryObject ProcessEqual(IQueryObject queryObject, Translator parameterizer)
        {
            var equal = queryObject as Equal;
            equal.Field = parameterizer.Process(equal.Field);
            equal.To = parameterizer.Process(equal.To);
            return equal;
        }

        public static IQueryObject ProcessFunction(IQueryObject queryObject, Translator parameterizer)
        {
            var function = queryObject as Function;
            function.ParameterList =
                                                  function
                                                      .ParameterList
                                                      .Select(o => parameterizer.Process(o))
                                                      .Cast<IQueryObject>()
                                                      .ToList();
            return function;
        }

        public static IQueryObject ProcessGreater(IQueryObject queryObject, Translator parameterizer)
        {
            var greater = queryObject as Greater;
            greater.Field = parameterizer.Process(greater.Field);
            greater.Than = parameterizer.Process(greater.Than);
            return greater;
        }

        public static IQueryObject ProcessGreaterOrEqual(IQueryObject queryObject, Translator parameterizer)
        {
            var greaterOrEqual = queryObject as GreaterOrEqual;
            greaterOrEqual.Field = parameterizer.Process(greaterOrEqual.Field);
            greaterOrEqual.Than = parameterizer.Process(greaterOrEqual.Than);
            return greaterOrEqual;
        }

        public static IQueryObject ProcessIfBlock(IQueryObject queryObject, Translator parameterizer)
        {
            var ifBlock = queryObject as IfBlock;
            ifBlock.Criteria = parameterizer.Process(ifBlock.Criteria) as SearchCriteria;
            ifBlock.ActionIfTrue = parameterizer.Process(ifBlock.ActionIfTrue);

            if (ifBlock.ActionIfFalse != null)
                ifBlock.ActionIfFalse = parameterizer.Process(ifBlock.ActionIfFalse);

            return ifBlock;
        }

        public static IQueryObject ProcessIn(IQueryObject queryObject, Translator parameterizer)
        {
            var valueIn = queryObject as In;
            valueIn.Field = parameterizer.Process(valueIn.Field);
            valueIn.Values = parameterizer.Process(valueIn.Values);
            return valueIn;
        }

        public static IQueryObject ProcessInsert(IQueryObject queryObject, Translator parameterizer)
        {
            var insert = queryObject as Insert;
            parameterizer.Process(insert.ColumnList);
            parameterizer.Process(insert.Table);
            if (insert.InsertValues != null && insert.InsertValues.Count > 0)
            {
                var parameterizedValues = parameterizer.Process(insert.InsertValues);
                insert.InsertValues.Clear();
                insert.InsertValues.AddRange(parameterizedValues as QueryList);
            }
            parameterizer.Process(insert.SelectQuery);
            return insert;
        }

        public static IQueryObject ProcessInsertColumn(IQueryObject queryObject, Translator parameterizer)
        {
            return queryObject;
        }

        public static IQueryObject ProcessLess(IQueryObject queryObject, Translator parameterizer)
        {
            var less = queryObject as Less;
            less.Field = parameterizer.Process(less.Field);
            less.Than = parameterizer.Process(less.Than);
            return less;
        }

        public static IQueryObject ProcessLessOrEqual(IQueryObject queryObject, Translator parameterizer)
        {
            var lessOrEqual = queryObject as LessOrEqual;
            lessOrEqual.Field = parameterizer.Process(lessOrEqual.Field);
            lessOrEqual.Than = parameterizer.Process(lessOrEqual.Than);
            return lessOrEqual;
        }

        public static IQueryObject ProcessLike(IQueryObject queryObject, Translator parameterizer)
        {
            var like = queryObject as Like;
            like.Field = parameterizer.Process(like.Field);
            like.Pattern = parameterizer.Process(like.Pattern);
            return like;
        }

        public static IQueryObject ProcessQueryList(IQueryObject queryObject, Translator parameterizer)
        {
            var list = queryObject as QueryList;

            return new QueryList()
                                      .AddRange(
                                      list
                                          .Cast<IQueryObject>()
                                          .Select(o => parameterizer.Process(o) ?? o));
        }

        private static IQueryObject ProcessRaiseError(IQueryObject queryObject, Translator parameterizer)
        {
            var raiseError = queryObject as RaiseError;
            return raiseError;
        }

        public static IQueryObject ProcessLiteral(IQueryObject queryObject, Translator parameterizer)
        {
            var literal = queryObject as Literal;
            return (Reference)parameterizer.GetParameter(literal.Value);
        }

        public static IQueryObject ProcessNotEqual(IQueryObject queryObject, Translator parameterizer)
        {
            var notEqual = queryObject as NotEqual;
            notEqual.Field = parameterizer.Process(notEqual.Field);
            notEqual.To = parameterizer.Process(notEqual.To);
            return notEqual;
        }

        public static IQueryObject ProcessNullQueryObject(IQueryObject query, Translator parameterizer)
        {
            return QueryValue.Reference("NULL");
        }

        public static IQueryObject ProcessOrder(IQueryObject queryObject, Translator parameterizer)
        {
            var order = queryObject as Order;
            return parameterizer.Process(order.ColumnList);
        }

        public static IQueryObject ProcessOrderColumn(IQueryObject queryObject, Translator parameterizer)
        {
            var orderColumn = queryObject as OrderColumn;

            return orderColumn;
        }

        public static IQueryObject ProcessPage(IQueryObject queryObject, Translator parameterizer)
        {
            var page = queryObject as Page;

            return page;
        }

        public static IQueryObject ProcessProperty(IQueryObject queryObject, Translator parameterizer)
        {
            var property = queryObject as Property;
            var assignment = parameterizer._repositoryMapIndex.GetMap(property.Type)
                                      .GetAssignmentsByPredicate(a => a.Target.Field == property)
                                      .FirstOrDefault();
            if (assignment != null)
            {
                return Column.Named(assignment.Source.Field).OwnedBy(assignment.Source.Entity);
            }
            return property;
        }

        public static IQueryObject ProcessReference(IQueryObject queryObject, Translator parameterizer)
        {
            var reference = queryObject as Reference;

            return reference;
        }

        public static IQueryObject ProcessSchemaColumn(IQueryObject queryObject, Translator parameterizer)
        {
            var schemaColumn = queryObject as SchemaColumn;

            return schemaColumn;
        }

        public static IQueryObject ProcessSearchCriteria(IQueryObject queryObject, Translator parameterizer)
        {
            var criteria = queryObject as SearchCriteria;
            criteria.Cast<IQueryObject>().ForEach(c => parameterizer.Process(c));
            return criteria;
        }

        public static IQueryObject ProcessSelect(IQueryObject queryObject, Translator parameterizer)
        {
            var select = queryObject as Select;
            parameterizer.Process(select.ColumnList);
            parameterizer.Process(select.Table);
            if (select.TopClause != null) parameterizer.Process(select.TopClause);
            parameterizer.Process(select.Qualifiers);
            return select;
        }

        public static IQueryObject ProcessTable(IQueryObject queryObject, Translator parameterizer)
        {
            var table = queryObject as Table;
            parameterizer.Process(table.JoinCriteria);
            table.JoinedTables.ForEach(j => parameterizer.Process(j));
            return table;
        }

        public static IQueryObject ProcessTableVariable(IQueryObject queryObject, Translator parameterizer)
        {
            var variable = queryObject as TableVariable;
            parameterizer.Process(variable.ColumnList);
            parameterizer.Process(variable.Table);
            return variable;
        }

        public static IQueryObject ProcessTop(IQueryObject queryObject, Translator parameterizer)
        {
            var top = queryObject as Top;
            top.Count = parameterizer.Process(top.Count);
            return top;
        }

        public static IQueryObject ProcessUpdate(IQueryObject queryObject, Translator parameterizer)
        {
            var update = queryObject as Update;
            parameterizer.Process(update.ColumnList);
            parameterizer.Process(update.Table);
            parameterizer.Process(update.Qualifiers);
            return update;
        }

        public static IQueryObject ProcessVariable(IQueryObject queryObject, Translator parameterizer)
        {
            var variable = queryObject as Variable;
            variable.Value = parameterizer.Process(variable.Value);
            return variable;
        }

        public static IQueryObject ProcessWhere(IQueryObject queryObject, Translator parameterizer)
        {
            var where = queryObject as Where;
            parameterizer.Process(where.SearchCriteria);
            return where;
        }
        #endregion

    }
}
